package com.gao.filter;

import com.gao.util.RedisKeys;
import com.gao.util.ResultCode;
import com.gao.util.ResultVO;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Set;

/**
 * @author gao
 * @date 2022/11/29 10:42:35
 */
@Slf4j
@Component

@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @NonNull
    private RedisTemplate redisTemplate;

    @NonNull
    private RestTemplate restTemplate;

    private PathMatcher pathMatcher = new AntPathMatcher();


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // 如果是预检请求，则直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return chain.filter(exchange);
        }

        // 获取看到要请求的path
        String path = request.getURI().getPath();
        log.info("请求的资源：{}", path);

        // 如果是登录请求，则直接放行。
        if (isLoginPath(pathMatcher, path)) {
            log.info("当前资源是登录端点，放行");
            return chain.filter(exchange);
        }

        // 如果请求的资源在白名单中，则直接放行
        if (isPublicUrls(pathMatcher, path)) {
            log.info("当前资源在白名单中，放行");
            return chain.filter(exchange);
        }

        // 判断请求中是否携带令牌，如果没有，则不放行
        String token = request.getHeaders().getFirst("Authorization");
        if (token == null) {
            log.info("没有携带token，不放行");
            return rejectRequest(exchange);
        }

        // 如果请求中携带令牌了，则让认证中心校验令牌，如果校验失败，则不放行
        ResultVO resultVO = restTemplate.postForObject("http://star-auth/auth/verify", token, ResultVO.class);
        if (resultVO.getCode().equals(ResultCode.FAILURE.getCode())) {
            log.info("携带的token是无效的");
            return rejectRequest(exchange);
        }

        // 获取该path所需要的权限
        Set<String> authorities = getPathAuthorities(path);
        log.info("资源所需权限：{}", authorities);

        // 如果不需要权限则直接放行
        if (authorities == null || authorities.size() == 0) {
            log.info("资源不需要权限，放行");
            return chain.filter(exchange);
        }

        // 如果需要权限，则查看令牌中携带的权限，是否包含访问path所需的权限
        resultVO = restTemplate.postForObject("http://star-auth/auth/getPermissionsByToken", token, ResultVO.class);
        List<String> permissions = (List<String>) resultVO.getData();
        log.info("请求所携带的权限：" + permissions);

        // 判断请求所携带的权限，是否包含资源所需的所有权限
        boolean b = permissions.containsAll(authorities);
        if (!b) {
            log.info("权限不够，无法访问资源");
            return rejectRequest(exchange);
        }

        log.info("权限足够，放行");
        return chain.filter(exchange);
    }


    private boolean isLoginPath(PathMatcher pathMatcher, String path) {
        return pathMatcher.match("/**/login/**", path);
    }

    private boolean isPublicUrls(PathMatcher pathMatcher, String path) {
        String publicUrlKey = RedisKeys.URL_PUBLIC + getServerPathPrefix(path);
        Set<String> publicUrls = redisTemplate.opsForSet().members(publicUrlKey);
        String unPrefixPath = path.replace(getServerPathPrefix(path), "");
        if (publicUrls.contains(unPrefixPath)) {
            return true;
        } else {
            for (String publicUrl : publicUrls) {
                if (pathMatcher.match(publicUrl, unPrefixPath)) {
                    return true;
                }
            }
        }
        return false;
    }

    private Set<String> getPathAuthorities(String path) {
        String key = RedisKeys.URL_AUTHORITIES + path;
        Set<String> authorities = redisTemplate.opsForSet().members(key);

        if (authorities == null || authorities.size() == 0) {
            authorities = getPathAuthoritiesByKeyPattern(path, authorities);
        }
        return authorities;
    }

    /**
     * 如果请求的资源是product-serv/products/1（用A表示），而在redis存放的资源是product-serv/products/{pid}（用B表示）
     * 则无法根据A来获取B，但是我们可以把B当做一个key pattern，来看看A是否匹配B，如果匹配，就获取B对应的权限
     *
     * @param path
     * @param authorities
     * @return
     */
    private Set<String> getPathAuthoritiesByKeyPattern(String path, Set<String> authorities) {
        String serverPathPrefix = getServerPathPrefix(path);
        String keyPatternPrefix = RedisKeys.URL_AUTHORITIES + serverPathPrefix;
        String keyPattern = keyPatternPrefix + "*";
        Set<String> keys = redisTemplate.keys(keyPattern);
        if (keys != null && keys.size() > 0) {
            for (String k : keys) {
                String k2 = k.substring((RedisKeys.URL_AUTHORITIES.length()));
                boolean match = pathMatcher.match(k2, path);
                if (match) {
                    authorities = redisTemplate.opsForSet().members(k);
                    break;
                }
            }
        }
        return authorities;
    }

    private String getServerPathPrefix(String path) {
        int firstSlashIndex = 0;
        int secondSlashIndex = path.substring(1).indexOf("/") + 1;
        String serverPath = path.substring(firstSlashIndex, secondSlashIndex);
        return serverPath;
    }

    private Mono<Void> rejectRequest(ServerWebExchange exchange) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
